package pl.put.to.classification.newton;

import java.io.IOException;
import java.util.Arrays;

import pl.put.to.classification.data.DataInstance;
import pl.put.to.classification.data.DataSet;
import pl.put.to.classification.utils.MatrixUtils;
import pl.put.to.classification.verification.StopConditionVeryficator;
import pl.put.to.classification.verification.error.IErrorVeryficator;
import Jama.Matrix;

public class NewtonRaphsonClassification {

	private double[] weights;
	private Matrix weightsVector;
	private IErrorVeryficator errorVeryficator;
	private StopConditionVeryficator stopConditionVeryficator;

	public NewtonRaphsonClassification(IErrorVeryficator errorVeryficator) {
		this.errorVeryficator = errorVeryficator;
		this.stopConditionVeryficator = new StopConditionVeryficator();
	}

	public void train(DataSet trainSet, DataSet verificationSet) throws IOException {
		double previousError = 0;
		double error = 0;

		boolean stop = false;

		long startTime = System.currentTimeMillis();
		long stopTime;
		weightsVector = prepareInitialWeightsVector(trainSet).transpose();

		while (!stop) {
			Matrix hesian = new Matrix(trainSet.getNumberOfVariables(), trainSet.getNumberOfVariables());
			Matrix gradient = new Matrix(trainSet.getNumberOfVariables(), 1);

			for (DataInstance instance : trainSet.getInstances()) {
				double beta = calculateBeta(instance, weightsVector);
				Matrix x = MatrixUtils.convertToVector(instance.getVariableValues()).transpose();
				int y = DataSet.determineY(instance.getClassVariableValue());

				Matrix hesianAddition = x.times(x.transpose()).times(beta * (1 - beta));
				hesian = hesian.plus(hesianAddition);

				Matrix gradientAddition = x.times(y).times(beta);
				gradient = gradient.plus(gradientAddition);
			}

			weightsVector = weightsVector.plus(hesian.inverse().times(gradient));

			error = errorVeryficator.countError(trainSet, weightsVector);

			stopTime = System.currentTimeMillis();

			System.out.println("time: " + (stopTime - startTime));
			System.out.println("train error: " + error);
			System.out.println("test error: " + test(verificationSet));
			System.out.println();

			stop = stopConditionVeryficator.verifyIfErrorNotChanging(error, previousError);

			previousError = error;
		}
		weights = MatrixUtils.convertToVectorArray(weightsVector.transpose());
	}

	public double test(DataSet testSet) {
		double error = errorVeryficator.countError(testSet, weightsVector);

		return error;
	}

	public double[] getWeights() {
		return weights;
	}

	private Matrix prepareInitialWeightsVector(DataSet data) {
		double[][] wArray = new double[1][data.getNumberOfVariables()];
		Arrays.fill(wArray[0], 0);
		Matrix w = new Matrix(wArray);

		return w;
	}

	private double calculateBeta(DataInstance instance, Matrix weights) {
		double beta = 0;

		int y = DataSet.determineY(instance.getClassVariableValue());
		Matrix x = MatrixUtils.convertToVector(instance.getVariableValues()).transpose();

		beta = 1 / (1 + Math.exp(MatrixUtils.convertToScalar(weights.transpose().times(x).times(y))));

		return beta;
	}
}
